package com.flow.framework.common.util.digest;

import com.flow.framework.common.constant.FrameworkCommonConstant;
import com.flow.framework.common.error.SystemErrorCode;
import com.flow.framework.common.exception.CheckedException;
import com.flow.framework.common.stream.handler.BatchVoidProcessHandler;
import com.flow.framework.common.util.digest.version.HmacVersion;
import com.flow.framework.common.util.io.IoUtil;
import com.flow.framework.common.util.verify.VerifyUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;

import javax.annotation.Nullable;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Optional;
import java.util.function.Consumer;

/**
 * 签名工具
 *
 * @author luoguopiao
 * @version 0.0.1
 * @date 2022/1/3
 */
@Slf4j
public final class SignatureUtil {

    /**
     * 签名，如果content为空，则是对空数组进行签名，此时secret必须为实时变化的值，如加上时间戳等
     *
     * @param version 版本
     * @param content 需要签名的内容
     * @param secret  秘钥
     * @return
     */
    public static String hmacSignByContent(HmacVersion version, @Nullable String content, String secret) {
        if (null == version || null == secret) {
            log.error("version or secret is null. version : {}", version);
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }

        try {
            String signatureAlgorithm = version.getSignatureAlgorithm();
            Mac mac = Mac.getInstance(signatureAlgorithm);
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), signatureAlgorithm));
            byte[] contentBytes = Optional.ofNullable(content)
                    .map(tempValue -> tempValue.getBytes(StandardCharsets.UTF_8))
                    .orElse(new byte[]{});

            // 对空数组进行签名
            byte[] signData = mac.doFinal(contentBytes);
            return Base64.encodeBase64String(signData);
        } catch (Exception e) {
            log.error("hmac sign error", e);
            throw new CheckedException(SystemErrorCode.REQUEST_SIGN_ERROR);
        }
    }

    /**
     * 签名
     *
     * @param version     版本
     * @param inputStream 输入流
     * @param secret      秘钥
     * @return
     */
    public static String hmacSignByStream(HmacVersion version, InputStream inputStream, String secret) {
        if (VerifyUtil.hasEmpty(version, inputStream, secret)) {
            log.error("version or secret is null. version : {}, {}", version,
                    null == inputStream ? "input stream is null" : "input stream isn't null");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }

        try {
            String signatureAlgorithm = version.getSignatureAlgorithm();
            Mac mac = Mac.getInstance(signatureAlgorithm);
            mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), signatureAlgorithm));
            digestUpdate(inputStream, mac::update);
            byte[] signData = mac.doFinal();
            return Base64.encodeBase64String(signData);
        } catch (Exception e) {
            log.error("hmac sign error", e);
            throw new CheckedException(SystemErrorCode.REQUEST_SIGN_ERROR);
        }
    }

    private static void digestUpdate(InputStream inputStream, Consumer<byte[]> consumer) {
        IoUtil.handleInputStream(inputStream, new BatchVoidProcessHandler() {
            @Override
            public int getBatchSize() {
                return FrameworkCommonConstant.DEFAULT_IO_BUFFER_SIZE;
            }

            @Override
            public void process(byte[] buffer) {
                consumer.accept(buffer);
            }
        }, exception -> {
            log.error("sign error.", exception);
            throw new CheckedException(SystemErrorCode.UNEXPECTED_ERROR, "sign error.", exception);
        });
    }

    /**
     * 签名验证，如果content为空，则是对空数组进行签名，此时secret必须为实时变化的值，如加上时间戳等
     *
     * @param version       版本
     * @param content       需要签名的内容
     * @param secret        秘钥
     * @param signedContent 签名后的内容
     * @return
     */
    public static boolean hmacVerifyByContent(HmacVersion version, @Nullable String content, String secret, String signedContent) {
        if (VerifyUtil.isEmpty(signedContent)) {
            log.error("signed content is empty.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        return signedContent.equals(hmacSignByContent(version, content, secret));
    }

    /**
     * 签名验证
     *
     * @param version       版本
     * @param inputStream   输入流
     * @param secret        秘钥
     * @param signedContent 签名后的内容
     * @return
     */
    public static boolean hmacVerifyByStream(HmacVersion version, InputStream inputStream, String secret, String signedContent) {
        if (VerifyUtil.isEmpty(signedContent)) {
            log.error("signed content is empty.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        return signedContent.equals(hmacSignByStream(version, inputStream, secret));
    }

    /**
     * 签名-调用此方法将对参数进行MD5签名
     *
     * @param content content
     * @return md5 md5
     */
    public static String md5SignByContent(String content) {
        if (null == content) {
            log.error("content is null.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            messageDigest.update(content.getBytes(StandardCharsets.UTF_8));
            return Base64.encodeBase64String(messageDigest.digest());
        } catch (Exception e) {
            log.error("sign error.", e);
            throw new CheckedException(SystemErrorCode.UNEXPECTED_ERROR, "sign error.", e);
        }
    }

    /**
     * 请调用方自行关闭输入流
     * 签名-调用此方法将对参数进行MD5签名
     *
     * @param inputStream inputStream
     * @return md5 md5
     */
    public static String md5SignByStream(InputStream inputStream) {
        if (null == inputStream) {
            log.error("input stream is null.");
            throw new CheckedException(SystemErrorCode.PARAMS_ERROR);
        }
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            digestUpdate(inputStream, messageDigest::update);
            return Base64.encodeBase64String(messageDigest.digest());
        } catch (Exception e) {
            log.error("sign error.", e);
            throw new CheckedException(SystemErrorCode.UNEXPECTED_ERROR, "sign error.", e);
        }
    }

    /**
     * 验证签名
     * 调用此方法验证签名是否一致
     *
     * @param content 传入参数
     * @param sign    传入签名
     * @return boolean  true 一致，false 不一致
     */
    public static boolean md5VerifyByContent(String content, String sign) {
        String temp = md5SignByContent(content);
        return temp.equals(sign);
    }

    /**
     * 验证签名
     * 调用此方法验证签名是否一致
     *
     * @param inputStream inputStream
     * @param sign        传入签名
     * @return boolean  true 一致，false 不一致
     */
    public static boolean md5VerifyByStream(InputStream inputStream, String sign) {
        String temp = md5SignByStream(inputStream);
        return temp.equals(sign);
    }
}